Skip to content

JSON 标准中 Map 的 key 必须是字符串类型

问题产生的背景:

一个计算分数的API。发生一个类型转换异常,并且是在第一次查询时会产生这个类型转换异常,后面如果再调用就不会发生这个异常。

问题代码

红色的部分会产生类型转换异常:

TypeScript
public class IndexSelectScore extends  BaseScore{


    @Autowired
    public IndexSelectScore(RedisCache redisCache, AppEduProjectStandardMapper mapper,
                            ObjectMapper objectMapper, RedissonClient redissonClient) {
        super(redisCache, mapper, objectMapper, redissonClient);
    }

    @Override
    public BigDecimal getScore(ExamScoreDto dto, EduExamProject examProject) {
        // 项目评分标准id
        String projectStandardId = parsePerformance(dto.getPerformance(), String.class);
        Map<Object, Object> projectStandard = getProjectStandard(dto);
        if (projectStandard.containsKey(projectStandardId)) {
            return new BigDecimal(projectStandard.get(projectStandardId).toString());
        }

        return new BigDecimal(0);
    }

    /**
     * @return
     */
    @Override
    public GradeInputType getType() {
        return GradeInputType.INDEX_SELECT;
    }

    /**
     * 构建成绩标准map
     *
     * @param list
     * @return
     */
    @Override
    protected Map<Object, Object> buildStandardMap(List<EduProjectStandard> list) {
        TreeMap<Object, Object> treeMap = new TreeMap<>();
        list.forEach(standard -> treeMap.put(standard.getId(), standard.getScore()));
        return treeMap;
    }

}

异常详情:

Java
请求地址'/app/standard/getScore',发生未知异常.
java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
        at java.lang.String.compareTo(String.java:111)
        at java.util.TreeMap.getEntry(TreeMap.java:352)
        at java.util.TreeMap.containsKey(TreeMap.java:232)
        at com.ruoyi.app.handler.score.IndexSelectScore.getScore(IndexSelectScore.java:39)
        at com.ruoyi.app.service.impl.AppEduProjectStandardServiceImpl.getScore(AppEduProjectStandardServiceImpl.java:183)
        at com.ruoyi.app.service.impl.AppEduProjectStandardServiceImpl$$FastClassBySpringCGLIB$$cbb2cdbd.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793)

问题描述:

在计算分数时如果Redis中没有该标准会从数据库中读取标准,然后进行分数计算。在Redis中没有分数时进行计算分数(也就是从数据库中读取时会产生这个问题),但是如果redis中有这个数据再从redis中读取时这个异常就不会发生。

为什么会产生这个问题?:

在数据库中读取回来的是Long类型,从redis中读取出来的是String类型。所以会发生类型转换异常。

问题的核心:

在缓存代码中进行了序列化,objectMapper.writeValueAsString(map) 序列化后 Redis 中的键会变成 String类型。

代码如下:

TypeScript
private void saveToRedis(String key, Map<Object, Object> map) {
    try {
        // 将Map序列化为JSON字符串
        String jsonStr = objectMapper.writeValueAsString(map);
        redisCache.setCacheObject(key, jsonStr, Constants.PROJECT_STANDARD_EXPIRE_TIME, TimeUnit.MINUTES);
    } catch (JsonProcessingException e) {
        log.error("项目评分标准序列化失败:{}", e);
        throw new ServiceException("项目评分标准序列化失败");
    }
}

在 JSON 格式中(RFC 8259),对象(也就是 Java 中的 Map)的 key 必须是字符串类型。例如:

JSON
{
  "1001": "张三",
  "1002": "李四"
}

即使在 Java 中写的是:

Java
Map<Long, String> map = new HashMap<>();
map.put(1001L, "张三");
map.put(1002L, "李四");

当用 Jackson(ObjectMapper)序列化为 JSON 字符串时,它会被转换成:

JSON
{
  "1001": "张三",
  "1002": "李四"
}

注意看 "1001" 是一个 字符串形式的 key,而不是数字或 Long 类型。

所以从 Redis 取出来再反序列化回来时:

调用类似这样的代码:

Java
String jsonStr = redisCache.getCacheObject(key);
Map<String, Object> map = objectMapper.readValue(jsonStr, new TypeReference<>() {});

结果就是:Redis 里存储的是 JSON 字符串,反序列化出来的 Map 的 key 就只能是 String 类型。

这就是为什么第一次从数据库读取时用的是 Long 或者 .toString() 得到的 String,但一旦经过 Redis 的序列化/反序列化之后,所有的 key 都变成了 String 类型 —— 这是 JSON 协议的天然限制。

解决方案:

使用 String 类型作为 Map 的 key

Java
@Override
protected Map<String, Object> buildStandardMap(List<EduProjectStandard> list) {
    TreeMap<String, Object> treeMap = new TreeMap<>();
    list.forEach(standard -> treeMap.put(standard.getId().toString(), standard.getScore()));
    return treeMap;
}

总结:

问题原因
Redis 中的 key 变成了 String 类型因为使用了 ObjectMapper 把 Map 序列化成 JSON,而 JSON 要求 key 是字符串
第一次正常,第二次报错因为第一次是从数据库来的数据,key 是 Long;第二次从 Redis 读取后变成 String,导致类型不一致
最近更新